<!-- Copyright (C) 2011 Google Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
    * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-->
<!DOCTYPE html>
<title>Test Runtimes</title>
<link rel='stylesheet' href='webtreemap.css'></link>
<style>
body {
    display: -moz-box;
    display: -webkit-box;
    display: box;
    -moz-box-orient: vertical;
    -webkit-box-orient: vertical;
    box-orient: vertical;

    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

td:first-child {
    text-align: left;
}

td {
    text-align: right;
}

#map {
    display: -moz-box;
    display: -webkit-box;
    display: box;

    -moz-box-flex: 1;
    -webkit-box-flex: 1;
    box-flex: 1;

    position: relative;
    cursor: pointer;
    -webkit-user-select: none;
    -moz-user-select: none;
}

.extra-dom {
    display: none;
    border: none;
    border-top: 1px dashed;
    padding: 4px;
    margin: 0;
    overflow: auto;
    cursor: auto;
    -webkit-user-select: text;
    -moz-user-select: text;
}

#focused-leaf {
    display: -webkit-box;
    display: -moz-box;
    -webkit-box-orient: vertical;
    -moz-box-orient: vertical;
}

#focused-leaf > .extra-dom {
    display: -webkit-box;
    display: -moz-box;
    -webkit-box-flex: 1;
    -moz-box-flex: 1;
}

#focused-leaf.webtreemap-node:hover {
    background: white;
}

#focused-leaf .webtreemap-caption:hover {
    background: #eee;
}

.error {
    color: red;
    font-style: italic;
}
</style>
<script src="builders.js"></script>
<script src="loader.js"></script>
<script src="dashboard_base.js"></script>
<script src='webtreemap.js'></script>

<div id='header-container'></div>
<p>Click on a box to zoom in. Click on the outermost box to zoom out. <a href="" onclick="showAverages();return false;">Show averages</a></p>
<div id='map'></div>

<script>
var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/";

function humanReadableTime(milliseconds)
{
    if (milliseconds < 1000)
        return Math.floor(milliseconds) + 'ms';
    else if (milliseconds < 60000)
        return (milliseconds / 1000).toPrecision(2) + 's';

    var minutes = Math.floor(milliseconds / 60000);
    var seconds = Math.floor((milliseconds - minutes * 60000) / 1000);
    return minutes + 'm' + seconds + 's';
}

// This looks like:
// { "data": {"$area": (sum of all timings)},
//   "name": (name of this node),
//   "children": [ (child nodes, in the same format as this) ] }
// childCount is added just to be includes in the node's name
function convertToWebTreemapFormat(treename, tree, path)
{
    var total = 0;
    var childCount = 0;
    var children = [];
    for (var name in tree) {
        var treeNode = tree[name];
        if (typeof treeNode == "number") {
            var time = treeNode;
            var node = {
                "data": {"$area": time},
                "name": name + " (" + humanReadableTime(time) + ")"
            };
            children.push(node);
            total += time;
            childCount++;
        } else {
            var newPath = path ? path + '/' + name : name;
            var subtree = convertToWebTreemapFormat(name, treeNode, newPath);
            children.push(subtree);
            total += subtree["data"]["$area"];
            childCount += subtree["childCount"];
        }
    }

    children.sort(function(a, b) {
        aTime = a.data["$area"]
        bTime = b.data["$area"]
        return bTime - aTime;
    });

    return {
        "data": {"$area": total},
        "name": treename + " (" + humanReadableTime(total) + " - " + childCount + " tests)",
        "children": children,
        "childCount": childCount,
        "path": path
    };
}

function listOfAllNonLeafNodes(tree, list)
{
    if (!tree.children)
        return;

    if (!list)
        list = [];
    list.push(tree);

    tree.children.forEach(function(child) {
        listOfAllNonLeafNodes(child, list);
    });
    return list;
}

function reverseSortByAverage(list)
{
    list.sort(function(a, b) {
        var avgA = a.data['$area'] / a.childCount;
        var avgB = b.data['$area'] / b.childCount;
        return avgB - avgA;
    });
}

function showAverages()
{
    if (!document.getElementById('map'))
        return;

    var table = document.createElement('table');
    table.innerHTML = '<th>directory</th><th># tests</th><th>avg time / test</th>';

    var allNodes = listOfAllNonLeafNodes(g_webTree);
    reverseSortByAverage(allNodes);
    allNodes.forEach(function(node) {
        var average = node.data['$area'] / node.childCount;
        if (average > 100 && node.childCount != 1) {
            var tr = document.createElement('tr');
            tr.innerHTML = '<td></td><td>' + node.childCount + '</td><td>' + humanReadableTime(average) + '</td>';
            tr.querySelector('td').innerText = node.path;
            table.appendChild(tr);
        }
    });

    var map = document.getElementById('map');
    map.parentNode.replaceChild(table, map);
}

var g_isGeneratingPage = false;
var g_webTree;

function generatePage()
{
    $('header-container').innerHTML = htmlForTestTypeSwitcher();

    g_isGeneratingPage = true;

    var rawTree = g_resultsByBuilder[g_currentState.builder];
    g_webTree = convertToWebTreemapFormat('LayoutTests', rawTree);
    appendTreemap($('map'), g_webTree);

    if (g_currentState.treemapfocus)
        focusPath(g_webTree, g_currentState.treemapfocus)

    g_isGeneratingPage = false;
}

function focusPath(tree, path)
{
    var parts = decodeURIComponent(path).split('/');
    if (extractName(tree) != parts[0]) {
        console.error('Could not focus tree rooted at ' + parts[0]);
        return;
    }

    for (var i = 1; i < parts.length; i++) {
        var children = tree.children;
        for (var j = 0; j < children.length; j++) {
            var child = children[j];
            if (extractName(child) == parts[i]) {
                tree = child;
                focus(tree);
                break;
            }
        }
        if (j == children.length) {
            console.error('Could not find tree at ' + parts[i]);
            break;
        }
    }

}

function handleValidHashParameter(key, value)
{
    switch(key) {
    case 'builder':
        validateParameter(g_currentState, key, value,
            function() { return value in g_builders; });
        return true;

    case 'treemapfocus':
        validateParameter(g_currentState, key, value,
            function() {
                // FIXME: There's probably a simpler regexp here. Just trying to match ascii + forward-slash.
                // e.g. LayoutTests/foo/bar.html
                return (value.match(/^(\w+\/\w*)*$/));
            });
        return true;

    default:
        return false;
    }
}

g_defaultDashboardSpecificStateValues = {
    treemapfocus: '',
}

function handleQueryParameterChange(params)
{
    for (var param in params) {
        if (param != 'treemapfocus') {
            $('map').innerHTML = 'Loading...';
            return true;
        }
    }
    return false;
}

// Overrides handleResourceLoadError in dashboard_base.js.
function handleResourceLoadError(builderName, e)
{
    $('map').innerHTML = '<span class=error>Could not load data for ' + builderName + '. ' +
        'Either there was a server-side error or ' + builderName + ' does not run ' +
        g_crossDashboardState.testType + '.</span>';
}

function extractName(node)
{
    return node.name.split(' ')[0];
}

function fullName(node)
{
    var buffer = [extractName(node)];
    while (node.parent) {
        node = node.parent;
        buffer.unshift(extractName(node));
    }
    return buffer.join('/');
}

function handleFocus(tree)
{
    var currentlyFocusedNode = $('focused-leaf');
    if (currentlyFocusedNode)
        currentlyFocusedNode.id = '';

    if (!tree.children)
        tree.dom.id = 'focused-leaf';

    var name = fullName(tree);

    if (!tree.children && !tree.extraDom && isLayoutTestResults()) {
        tree.extraDom = document.createElement('pre');
        tree.extraDom.className = 'extra-dom';
        tree.dom.appendChild(tree.extraDom);

        loader.request(TEST_URL_BASE_PATH + name,
            function(xhr) {
                tree.extraDom.onmousedown = function(e) {
                    e.stopPropagation();
                };
                tree.extraDom.textContent = xhr.responseText;
            },
            function (xhr) {
                tree.extraDom.textContent = "Could not load test."
        });
    }

    // We don't want the focus calls during generatePage to try to modify the query state.
    if (!g_isGeneratingPage)
        setQueryParameter('treemapfocus', name);
}
</script>
